Note The following discussion of value types and reference types assumes that you have a background in object-oriented programming. If this is not the case, you may wish to skip to the final section in this chapter (Understanding C# Nullable Types) and return to this section once you have read Chapters 5 and 6.
Unlike arrays, strings, or enumerations, C# structures do not have an identically named representation in the .NET library (that is, there is no System.Structure class), but are implicitly derived from System.ValueType. Simply put, the role of System.ValueType is to ensure that the derived type (e.g., any structure) is allocated on the stack rather than the garbage collected heap. Simply put, data allocated on the stack can be created and destroyed very quickly, as its lifetime is determined by the defining scope. Heap allocated data, on the other hand, is monitored by the .NET garbage collector, and has a lifetime that is determined by a large number of factors.
Functionally, the only purpose of System.ValueType is to override the virtual methods defined by System.Object to use value-based, versus reference-based, semantics. As you may know, overriding is the process of changing the implementation of a virtual (or possibly abstract) method defined within a base class. The base class of ValueType is System.Object. In fact, the instance methods defined by System.ValueType are identical to those of System.Object:
// Structures and enumerations implicitly extend System.ValueType. public abstract class ValueType : object { public virtual bool Equals(object obj); public virtual int GetHashCode(); public Type GetType(); public virtual string ToString(); }
Given the fact that value types are using value-based semantics, the lifetime of a structure (which includes all numerical data types [int, float], as well as any enum or custom structure) is very predictable. When a structure variable falls out of the defining scope, it is removed from memory immediately:
// Local structures are popped off // the stack when a method returns. static void LocalValueTypes() { // Recall! "int" is really a System.Int32 structure. int i = 0; // Recall! Point is a structure type. Point p = new Point(); } // "i" and "p" popped off the stack here!
When you assign one value type to another, a member-by-member copy of the field data is achieved. In the case of a simple data type such as System.Int32, the only member to copy is the numerical value. However, in the case of your Point, the X and Y values are copied into the new structure variable. To illustrate, create a new Console Application project named ValueAndReferenceTypes, then copy your previous Point definition into your new namespace. Next, add the following method to your Program type:
// Assigning two intrinsic value types results in // two independent variables on the stack. static void ValueTypeAssignment() { Console.WriteLine("Assigning value types\n"); Point p1 = new Point(10, 10); Point p2 = p1; // Print both points. p1.Display(); p2.Display(); // Change p1.X and print again. p2.X is not changed. p1.X = 100; Console.WriteLine("\n=> Changed p1.X\n"); p1.Display(); p2.Display(); }
Here you have created a variable of type Point (named p1) that is then assigned to another Point (p2). Because Point is a value type, you have two copies of the MyPoint type on the stack, each of which can be independently manipulated. Therefore, when you change the value of p1.X, the value of p2.X is unaffected:
Assigning value types X = 10, Y = 10 X = 10, Y = 10 => Changed p1.X X = 100, Y = 10 X = 10, Y = 10
In stark contrast to value types, when you apply the assignment operator to reference types (meaning all class instances), you are redirecting what the reference variable points to in memory. To illustrate, create a new class type named PointRef that has the exact same members as the Point structures, beyond renaming the constructor to match the class name:
// Classes are always reference types. class PointRef { // Same members as the Point structure... // Be sure to change your constructor name to PointRef! public PointRef(int XPos, int YPos) { X = XPos; Y = YPos; } }
Now, make use of your PointRef type within the following new method. Note that beyond using the PointRef class, rather than the Point structure, the code is identical to the ValueTypeAssignment() method.
static void ReferenceTypeAssignment() { Console.WriteLine("Assigning reference types\n"); PointRef p1 = new PointRef(10, 10); PointRef p2 = p1; // Print both point refs. p1.Display(); p2.Display(); // Change p1.X and print again. p1.X = 100; Console.WriteLine("\n=> Changed p1.X\n"); p1.Display(); p2.Display(); }
In this case, you have two references pointing to the same object on the managed heap. Therefore, when you change the value of X using the p2 reference, p1.X reports the same value. Assuming you have called this new method within Main(), your output should look like the following:
Assigning reference types X = 10, Y = 10 X = 10, Y = 10 => Changed p1.X X = 100, Y = 10 X = 100, Y = 10
Now that you have a better feeling for the basic differences between value types and reference types, let's examine a more complex example. Assume you have the following reference (class) type that maintains an informational string that can be set using a custom constructor:
class ShapeInfo { public string infoString; public ShapeInfo(string info) { infoString = info; } }
Now assume that you want to contain a variable of this class type within a value type named Rectangle. To allow the caller to set the value of the inner ShapeInfo member variable, you also provide a custom constructor. Here is the complete definition of the Rectangle type:
struct Rectangle { // The Rectangle structure contains a reference type member. public ShapeInfo rectInfo; public int rectTop, rectLeft, rectBottom, rectRight; public Rectangle(string info, int top, int left, int bottom, int right) { rectInfo = new ShapeInfo(info); rectTop = top; rectBottom = bottom; rectLeft = left; rectRight = right; } public void Display() { Console.WriteLine("String = {0}, Top = {1}, Bottom = {2}, " + "Left = {3}, Right = {4}", rectInfo.infoString, rectTop, rectBottom, rectLeft, rectRight); } }
At this point, you have contained a reference type within a value type. The million dollar question now becomes, what happens if you assign one Rectangle variable to another?Given what you already know about value types, you would be correct in assuming that the integer data (which is indeed a structure) should be an independent entity for each Rectangle variable. But what about the internal reference type? Will the object's state be fully copied, or will the reference to that object be copied? To answer this question, define the following method and invoke it from Main().
static void ValueTypeContainingRefType() { // Create the first Rectangle. Console.WriteLine("-> Creating r1"); Rectangle r1 = new Rectangle("First Rect", 10, 10, 50, 50); // Now assign a new Rectangle to r1. Console.WriteLine("-> Assigning r2 to r1"); Rectangle r2 = r1; // Change some values of r2. Console.WriteLine("-> Changing values of r2"); r2.rectInfo.infoString = "This is new info!"; r2.rectBottom = 4444; // Print values of both rectangles. r1.Display(); r2.Display(); }
The output can been seen in the following:
-> Creating r1 -> Assigning r2 to r1 -> Changing values of r2 String = This is new info!, Top = 10, Bottom = 50, Left = 10, Right = 50 String = This is new info!, Top = 10, Bottom = 4444, Left = 10, Right = 50
As you can see, when you change the value of the informational string using the r2 reference, the r1 reference displays the same value. By default, when a value type contains other reference types, assignment results in a copy of the references. In this way, you have two independent structures, each of which contains a reference pointing to the same object in memory (i.e., a shallow copy). When you want to perform a deep copy, where the state of internal references is fully copied into a new object, one approach is to implement the ICloneable interface
"Reference types or value types can obviously be passed as parameters to methods. However, passing a reference type (e.g., a class) by reference is quite different from passing it by value. To understand the distinction, assume you have a simple Person class defined in a new Console Application project named RefTypeValTypeParams, defined as follows:
class Person { public string personName; public int personAge; // Constructors. public Person(string name, int age) { personName = name; personAge = age; } public Person(){} public void Display() { Console.WriteLine("Name: {0}, Age: {1}", personName, personAge); } }
Now, what if you create a method that allows the caller to send in the Person object by value (note the lack of parameter modifiers, such as out or ref):
static void SendAPersonByValue(Person p) { // Change the age of "p"? p.personAge = 99; // Will the caller see this reassignment? p = new Person("Nikki", 99); }
Notice how the SendAPersonByValue() method attempts to reassign the incoming Person reference to a new Person object as well as change some state data. Now let's test this method using the following Main() method:
static void Main(string[] args) { // Passing ref-types by value. Console.WriteLine("***** Passing Person object by value *****"); Person fred = new Person("Fred", 12); Console.WriteLine("\nBefore by value call, Person is:"); fred.Display(); SendAPersonByValue(fred); Console.WriteLine("\nAfter by value call, Person is:"); fred.Display(); Console.ReadLine(); }
The following is the output of this call.
***** Passing Person object by value ***** Before by value call, Person is: Name: Fred, Age: 12 After by value call, Person is: Name: Fred, Age: 133
As you can see, the value of personAge has been modified. This behavior seems to fly in the face of what it means to pass a parameter "by value." Given that you were able to change the state of the incoming Person, what was copied? The answer: a copy of the reference to the caller's object. Therefore, as the SendAPersonByValue() method is pointing to the same object as the caller, it is possible to alter the object's state data. What is not possible is to reassign what the reference is pointing to.
Now assume you have a SendAPersonByReference() method, which passes a reference type by reference (note the ref parameter modifier):
static void SendAPersonByReference(ref Person p) { // Change some data of "p". p.personAge = 555; // "p" is now pointing to a new object on the heap! p = new Person("Nikki", 999); }
As you might expect, this allows complete flexibility of how the callee is able to manipulate the incoming parameter. Not only can the callee change the state of the object, but if it so chooses, it may also reassign the reference to a new Person type. Now ponder the following updated Main() method:
static void Main(string[] args) { // Passing ref-types by ref. Console.WriteLine("***** Passing Person object by reference *****"); Person mel = new Person("Mel", 23); Console.WriteLine("Before by ref call, Person is:"); mel.Display(); SendAPersonByReference(ref mel); Console.WriteLine("After by ref call, Person is:"); mel.Display(); Console.ReadLine(); }
Notice the following output:
***** Passing Person object by reference ***** Before by ref call, Person is: Name: Mel, Age: 23 After by ref call, Person is: Name: Nikki, Age: 999
As you can see, an object named Mel returns after the call as an object named Nikki, as the method was able to change what the incoming reference pointed to in memory. The golden rule to keep in mind when passing reference types is the following:
To wrap up this topic, consider the information in Table 4-3, which summarizes the core distinctions between value types and reference types.
Table 4-3. Value Types and Reference Types Side by Side
Intriguing Question | Value Type | Reference Type |
---|---|---|
Where is this type allocated? | Allocated on the stack. | Allocated on the managed heap. |
How is a variable represented? | Value type variables are local copies. | Reference type variables are pointing to the memory occupied by the allocated instance. |
What is the base type? | Must derive from System.ValueType. | Can derive from any other type (except System.ValueType), as long as that type is not "sealed". |
Can this type function as a base to other types? | No. Value types are always sealed and cannot be inherited from. | Yes. If the type is not sealed, it may function as a base to other types. |
What is the default parameter passing behavior? | Variables are passed by value (i.e., a copy of the variable is passed into the called function). | For value types, the object is copied-by-value. For reference types, the reference is copied-by-value. |
Can this type override System.Object.Finalize()? | No. Value types are never placed onto the heap and therefore do not need to be finalized. | Yes, indirectly. |
Can I define constructors for this type? | Yes, but the default constructor is reserved (i.e., your custom constructors must all have arguments). | But of course! |
When do variables of this type die? | When they fall out of the defining scope | When the object is garbage collected. |
Despite their differences, value types and reference types both have the ability to implement interfaces and may support any number of fields, methods, overloaded operators, constants, properties, and events.